Go 语言学习-“函数式编程”
这个标题起得不是很好,Golang 不是就是函数式的吗?为啥还取这个名称,这里主要是用于类别 Java 那种函数式编程(传递回调函数),与正常的编程方式
Go 的闭包
Go 语言支持匿名函数,可作为闭包。这个很像 JS 那个闭包
func getSequence() func() int {
i := 0
return func() int {
i += 1
return i
}
}
func main() {
/* nextNumber 为一个函数,函数 i 为 0 */
nextNumber := getSequence()
/* 调用 nextNumber 函数,i 变量自增 1 并返回 */
fmt.Println(nextNumber()) // 1
fmt.Println(nextNumber()) // 2
fmt.Println(nextNumber()) // 3
/* 创建新的函数 nextNumber1,并查看结果 */
nextNumber1 := getSequence()
fmt.Println(nextNumber1()) // 1
fmt.Println(nextNumber1()) // 2
// 当然这个闭包也可以像 JS 那样玩
i := getSequence()()
fmt.Println(i) // 1
}
不过我个人理解,Go 中没有类型,无法使用类来包装(隔离)一些参数,所以可以使用闭包的方式缩小变量作用域,减少对全局变量的污染。
一个闭包相当于一个类的实例,函数体之外的变量相当于这个实例存储的变量。
所以某种程度上可以用闭包代替对象的操作
其实闭包其实使用还是很频繁的,例如下面这种匿名函数的场景,也是通过闭包的特性直接访问了 waitGroup
func main() {
var waitGroup sync.WaitGroup
waitGroup.Add(100)
for i := 0; i < 100; i++ {
go func() {
// do something....
waitGroup.Done()
}()
}
waitGroup.Wait()
}
因为有了闭包,函数可以直接访问到那些没有作为参数传入的变量。匿名函数并没有拿到这些变量的副本,而是直接访问外层函数作用域中声明的这些变量本身。
快速使用 Go 的回调函数
// 声明一个函数类型
type cb func(int) int
func testCallBack(x int, f cb) {
f(x)
}
func callBack(x int) int {
fmt.Printf("我是回调,x:%d\n", x)
return x
}
func main() {
// 传入同参方法
testCallBack(1, callBack)
// 或者直接使用匿名函数的方式
testCallBack(2, func(x int) int {
fmt.Printf("我是回调,x:%d\n", x)
return x
})
}
接口型函数
接口型函数,指的是 用函数实现接口,这样在调用的时候就会非常简便,我称这种函数,为接口型函数,这种方式使用于只有一个函数的接口。
如下代码
// A Getter loads data for a key.
type Getter interface {
Get(key string) ([]byte, error)
}
// A GetterFunc implements Getter with a function.
type GetterFunc func(key string) ([]byte, error)
// Get implements Getter interface function
func (f GetterFunc) Get(key string) ([]byte, error) {
return f(key)
}
接口型函数只能应用于接口内部只定义了一个方法的情况,例如接口 Getter 内部有且只有一个方法 Get。既然只有一个方法,为什么还要多此一举,封装为一个接口呢?定义参数的时候,直接用 GetterFunc 这个函数类型不就好了,让用户直接传入一个函数作为参数,不更简单吗?
所以呢,接口型函数的价值什么?
先说结论,接口型函数既能够将普通的函数类型(需类型转换)作为参数,也可以将结构体作为参数,使用更为灵活,可读性也更好,这就是接口型函数的价值。
使用接口的方式可以保存很多上下文信息,而函数不行(闭包也可以,但是那个太麻烦了),最明显的例子就是 error 接口
type error interface {
Error() string
}
这样各个结构体实现了这个 Error()
方法就可以使用结构体内部的状态,而只使用回调函数是做不到的。
继续上面那个 Getter
接口的例子,它有多种方式调用该函数:
方式一:传递普通回调函数
这种场景可以直接使用回调函数就行了,没有必要使用到接口型函数
传递一个匿名回调函数
GetFromSource(GetterFunc(func(key string) ([]byte, error) {
return []byte(key), nil
}), "hello")
传递一个普通的函数
func test(key string) ([]byte, error) {
return []byte(key), nil
}
func main() {
GetFromSource(GetterFunc(test), "hello")
}
方式二:结构体方法
这种才是它正常的使用场景:
实现了 Getter 接口的结构体作为参数
type DB struct{
url string
// ... 很多属性
}
func (db *DB) Query(sql string, args ...string) string {
// ...
return "hello"
}
// 实现了 Getter 接口
func (db *DB) Get(key string) ([]byte, error) {
// ...
v := db.Query("SELECT NAME FROM TABLE WHEN NAME= ?", key)
return []byte(v), nil
}
func main() {
db := new(DB)
GetFromSource(db, "hello")
}
如上,就可以在这个 Get 方法里面使用到结构体里面的属性了。
这样,既能够将普通的函数类型(需类型转换)作为参数,也可以将结构体作为参数,使用更为灵活,可读性也更好,这就是接口型函数的价值。
柯里化
这个柯里化也算是老面孔了,在 js 时就经常看到这个东西,实际上和上面闭包是一个概念的东西,都是在外层的函数保存状态,然后内层的函数复用这个状态
柯里化,英语:Currying,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
这个解释有一点抽象,就拿被做了无数次示例的 add 函数,来做一个简单的实现。
import "fmt"
// 普通的add函数
func add(x, y int) int {
return x + y
}
// Currying后
func curryingAdd(x int) func(int) int {
return func(y int) int {
return x + y
}
}
func main() {
fmt.Println(add(1, 2)) // 3
fmt.Println(curryingAdd(1)(2)) // 3
}
实际上就是把 add 函数的 x,y 两个参数变成了先用一个函数接收 x 然后返回一个函数去处理 y 参数。现在思路应该就比较清晰了,就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
Currying 有哪些好处呢? ,这里直接使用 js 的例子了,反正概念是一样的
参数复用
// 正常正则验证字符串 reg.test(txt)
// 函数封装后
function check(reg, txt) {
return reg.test(txt)
}
check(/\d+/g, 'test') //false
check(/[a-z]+/g, 'test') //true
// Currying后
function curryingCheck(reg) {
return function(txt) {
return reg.test(txt)
}
}
var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)
hasNumber('test1') // true
hasNumber('testtest') // false
hasLetter('21212') // false
上面的示例是一个正则的校验,正常来说直接调用 check 函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数 reg 进行复用,这样别的地方就能够直接调用 hasNumber,hasLetter 等函数,让参数能够复用,调用起来也更方便。
使用任意数量的参数
这个例子和上面是一样的
func mkAdd(a int) func(...int) int {
return func(b... int) int {
for _, i := range b {
a += i
}
return a
}
}
func main() {
add2 := mkAdd(2)
add3 := mkAdd(3)
fmt.Println(add2(5,3), add3(6))
}
延迟计算
不断的柯里化,累积传入的参数,最后执行。
举个例子:
// Currying后
func curryingAdd() func(int) func() int {
arr := []int{}
// 这里只是记录参数
return func(y int) func() int {
arr = append(arr, y)
// 最后调用这个才计算结果
return func() int {
reslut := 0
for _, v := range arr {
reslut += v
}
return reslut
}
}
}
func main() {
mkadd := curryingAdd()
for i := 0; i < 100; i++ {
mkadd(i)
}
fmt.Println(mkadd(0)()) // 这里才计算结果
}